Ce tutorial fait suite au codelab oo-04a-heritage-intro (Partie A) et aux slides Partie B de l'héritage. Il se concentre sur la spécialisation du comportement : virtual, override, et le polymorphisme.
Si vous n'avez pas encore fait oo-04a-heritage-intro, revenez en arrière : ce codelab part du principe que vous savez déjà créer une classe dérivée et appeler base(...).
Ce tutorial reprend le code du codelab oo-03-encapsulation (escargot encapsulé) et introduit les classes spécialisées (FastSnail, SlowSnail, BonusSnail) avec redéfinition de comportement.
À la fin du codelab précédent, la classe Snail est encapsulée :
class Snail
{
private int _energy;
public string Name { get; }
public ConsoleColor Color { get; }
public int X { get; private set; }
public int Y { get; private set; }
public int Energy
{
get { return _energy; }
private set { /* validation [10, 100] */ }
}
public Snail(string name, ConsoleColor color, int x, int y) { ... }
public void Move(int dx, int dy) { ... }
public void ReduceEnergy(int amount) { ... }
public void Draw() { ... }
public void Erase() { ... }
}
Le problème : tous les escargots se comportent exactement de la même manière. On aimerait des types spécialisés sans copier-coller la classe.
À travers 5 étapes progressives, vous apprendrez à :
protected set, virtual): Snail et base(...)overrideModifier la classe Snail pour qu'elle puisse servir de classe de base : les classes dérivées doivent pouvoir modifier certaines propriétés et redéfinir Move().
private set en protected setOuvrez Snail.cs et modifiez les propriétés X, Y et Energy :
class Snail
{
private int _energy;
public string Name { get; }
public ConsoleColor Color { get; }
public int X { get; protected set; } // Changé : private → protected
public int Y { get; protected set; } // Changé : private → protected
public int Energy
{
get { return _energy; }
protected set // Changé : private → protected
{
if (value < 10)
_energy = 10;
else if (value > 100)
_energy = 100;
else
_energy = value;
}
}
// ... constructeur et méthodes inchangés pour l'instant
}
protectedAu chapitre encapsulation, on avait vu ce modificateur dans le tableau sans l'expliquer :
| Partout |
| Uniquement dans la classe |
| Dans la classe et ses classes dérivées |
protected set signifie : les classes dérivées (comme FastSnail) pourront modifier X, Y et Energy, mais le code extérieur (Program.cs) ne pourra toujours pas.
Move() virtuelAjoutez le mot-clé virtual à la méthode Move() :
public virtual void Move(int dx, int dy)
{
X = X + dx;
Y = Y + dy;
}
virtual signifie : "les classes dérivées peuvent remplacer cette méthode par leur propre version."
Compiler et exécuter. La course fonctionne exactement comme avant. On n'a rien cassé — on a juste préparé Snail pour accueillir des classes dérivées.
Créer la première classe dérivée : un escargot rapide qui avance 2 fois plus vite.
Créez un nouveau fichier FastSnail.cs dans le même projet :
class FastSnail : Snail
{
public FastSnail(string name, ConsoleColor color, int x, int y)
: base(name, color, x, y)
{
}
public override void Move(int dx, int dy)
{
base.Move(dx * 2, dy); // Double la distance horizontale
}
}
|
|
| Appelle le constructeur de |
| Remplace le |
| Appelle le |
Dans Program.cs ou dans Race.Init(), remplacer un escargot par un FastSnail :
Snail[] snails = new Snail[]
{
new Snail("Normal", ConsoleColor.White, 0, 2),
new FastSnail("Turbo", ConsoleColor.Yellow, 0, 4), // FastSnail !
new Snail("Flash", ConsoleColor.Magenta, 0, 6)
};
Compiler et exécuter. Turbo avance 2 fois plus vite que les autres. Notez que le tableau est de type Snail[] mais contient un FastSnail — c'est le début du polymorphisme.
Créer deux classes dérivées supplémentaires avec des comportements différents.
Créez SlowSnail.cs :
class SlowSnail : Snail
{
public SlowSnail(string name, ConsoleColor color, int x, int y)
: base(name, color, x, y)
{
}
public override void Move(int dx, int dy)
{
if (Energy > 30)
{
base.Move(dx, dy); // Avance normalement
}
// Sinon : ne bouge pas du tout (trop fatigué)
}
}
SlowSnail utilise Energy (lecture publique) pour décider s'il bouge. Quand l'énergie descend sous 30, il s'arrête complètement.
Créez BonusSnail.cs :
class BonusSnail : Snail
{
private Random _rng = new Random();
public BonusSnail(string name, ConsoleColor color, int x, int y)
: base(name, color, x, y)
{
}
public override void Move(int dx, int dy)
{
int bonus = _rng.Next(0, 3); // Bonus entre 0 et 2
base.Move(dx + bonus, dy); // Avance de dx + bonus
}
}
BonusSnail a un champ _rng en plus de ce qu'il hérite de Snail. À chaque déplacement, il ajoute un bonus aléatoire.
Mettez à jour le tableau d'escargots :
Snail[] snails = new Snail[]
{
new Snail("Normal", ConsoleColor.White, 0, 2),
new FastSnail("Turbo", ConsoleColor.Yellow, 0, 4),
new SlowSnail("Papy", ConsoleColor.Gray, 0, 6),
new BonusSnail("Lucky", ConsoleColor.Green, 0, 8)
};
Compilez et exécutez. Observez les comportements différents :
Comprendre et exploiter le polymorphisme : la boucle de course traite tous les escargots de manière uniforme, sans se soucier de leur type.
Random rng = new Random();
bool raceOver = false;
while (!raceOver)
{
foreach (Snail snail in snails)
{
snail.Erase();
int dx = rng.Next(1, 4);
snail.Move(dx, 0); // Polymorphisme ici !
snail.ReduceEnergy(dx);
snail.Draw();
if (snail.X >= Race.FinishX)
{
raceOver = true;
Console.WriteLine($"{snail.Name} a gagné !");
break;
}
}
Thread.Sleep(100);
}
Move() est appelé ?Quand la boucle exécute snail.Move(dx, 0), C# regarde le type réel de l'objet (pas le type de la variable) :
|
|
|
|
|
|
|
|
|
|
|
|
Sans polymorphisme, il faudrait écrire :
// MAUVAIS : sans polymorphisme
if (snail is FastSnail fast)
fast.Move(dx, 0);
else if (snail is SlowSnail slow)
slow.Move(dx, 0);
else if (snail is BonusSnail bonus)
bonus.Move(dx, 0);
else
snail.Move(dx, 0);
Avec le polymorphisme : une seule lignesnail.Move(dx, 0). Et si on ajoute un 5ᵉ type d'escargot demain, la boucle ne change pas.
Ajouter un compteur qui affiche le nombre de tours de la course. Vérifier que tous les types fonctionnent correctement :
int turn = 0;
while (!raceOver)
{
turn++;
Console.SetCursorPosition(0, 0);
Console.Write($"Tour {turn}");
foreach (Snail snail in snails)
{
// ... même code qu'avant
}
Thread.Sleep(100);
}
Ajouter des membres spécifiques à BonusSnail : un compteur de bonus et une méthode CollectBonus() qui récupère de l'énergie.
Modifier BonusSnail.cs :
class BonusSnail : Snail
{
private Random _rng = new Random();
public int BonusCount { get; private set; }
public BonusSnail(string name, ConsoleColor color, int x, int y)
: base(name, color, x, y)
{
BonusCount = 0;
}
public override void Move(int dx, int dy)
{
int bonus = _rng.Next(0, 3);
base.Move(dx + bonus, dy);
}
public void CollectBonus()
{
Energy = Energy + 10; // protected set → autorisé !
BonusCount = BonusCount + 1;
}
}
CollectBonus() utilise protected set sur Energy pour ajouter de l'énergie. La validation dans la propriété garantit que ça ne dépasse pas 100.
is pour accéder aux membres spécifiquesDans la boucle de course, snail est de type Snail. Pour appeler CollectBonus(), il faut vérifier le type réel avec is :
foreach (Snail snail in snails)
{
snail.Erase();
int dx = rng.Next(1, 4);
snail.Move(dx, 0);
snail.ReduceEnergy(dx);
// Comportement spécifique à BonusSnail
if (snail is BonusSnail bonusSnail)
{
if (rng.Next(0, 5) == 0) // 1 chance sur 5
{
bonusSnail.CollectBonus();
}
}
snail.Draw();
}
isis fait deux choses en une :
snail est réellement un BonusSnailbonusSnail si c'est le casif (snail is BonusSnail bonusSnail)
{
// Ici, bonusSnail est de type BonusSnail
// On peut accéder à BonusCount et CollectBonus()
Console.WriteLine($"Bonus #{bonusSnail.BonusCount}");
}
Lucky devrait parfois récupérer de l'énergie pendant la course, lui donnant un avantage sur la durée.
Ajoutez une méthode ToString() dans Snail et redéfinissez-la dans les dérivées :
// Dans Snail :
public override string ToString()
{
return $"{Name} (x={X}, energy={Energy})";
}
// Dans FastSnail :
public override string ToString()
{
return $"{Name} [RAPIDE] (x={X}, energy={Energy})";
}
// Dans BonusSnail :
public override string ToString()
{
return $"{Name} [BONUS x{BonusCount}] (x={X}, energy={Energy})";
}
ToString() est une méthode virtual de la classe object (dont toutes les classes héritent). En la redéfinissant avec override, on personnalise l'affichage.
Créez un PoisonSnail qui empoisonne l'escargot devant lui :
class PoisonSnail : Snail
{
public PoisonSnail(string name, ConsoleColor color, int x, int y)
: base(name, color, x, y)
{
}
public override void Move(int dx, int dy)
{
base.Move(dx, dy); // Avance normalement
}
public void Poison(Snail target)
{
target.ReduceEnergy(15); // Fait perdre 15 d'énergie à la cible
}
}
Chaque override de Move() appelle base.Move(...) avec des paramètres différents. C'est un pattern courant : la dérivée enrichit le comportement de base au lieu de le remplacer complètement.
Essayez de créer un ZigZagSnail qui avance en zigzag :
class ZigZagSnail : Snail
{
private int _direction = 1;
public ZigZagSnail(string name, ConsoleColor color, int x, int y)
: base(name, color, x, y)
{
}
public override void Move(int dx, int dy)
{
base.Move(dx, _direction); // Alterne haut/bas
_direction = _direction * -1;
}
}
Types d'escargots | 1 seul ( | 4+ types spécialisés |
Comportement Move | Identique pour tous | Spécialisé par type ( |
Ajout d'un type | Copier-coller toute la classe | Créer une classe |
Boucle de course |
|
|
Duplication | Massive si plusieurs types | Zéro |
Accès dérivées |
|
|
: Snail crée une classe dérivée qui hérite de toutbase(...) appelle le constructeur de la classe parenteprotected ouvre l'accès aux dérivées, pas à l'extérieurvirtual autorise la redéfinition d'une méthodeoverride remplace le comportement dans la dérivéebase.Move(...) appelle la version originaleSnail[] contient tous les types, la boucle est identiqueis permet de vérifier et convertir le type réelSnail peut encore être instancié directement. Mais un escargot "générique" a-t-il du sens ? Les classes abstraites forceront chaque type à implémenter son propre comportement.